import * as Othello from "../domain/othello";
import * as OthelloImpl from "../domain/othello.Impl";
import * as Vector from "../common/vector";
import { Stack } from "immutable";

namespace Graphic {
    export function Show(state: State): State {
        process.stdout.write('\x1Bc');

        const board = state.game.getCurrentBoard();
        const printRows = [
            firstRow,
            ...board
                .map((r, ri) => ` ${r.map(cell2Str).map((c, ci) => (ri == state.selected.x && ci == state.selected.y) ? `[${c}]` : ` ${c} `).join(" ")} `)
                .reduce<string[]>((arr, c, i) => arr.concat(i == 0 ? [c] : [midRow, c]), new Array<string>()),
            lastRow,
            "",
            CountMessage(board, 'White'),
            CountMessage(board, 'Black'),
            ...(state.messages == undefined ? [""] : ["", ...state.messages]),
            OnTurnMessage(state.game.getCurrentTurn())
        ];

        for (const r of printRows)
            console.log(r);

        return state;
    }

    function cell2Str(cell: Othello.Cell) {
        switch (cell) {
            case "White": return "●";
            case "Black": return "○";
            case "Empty": return " ";
        }
    }

    function CountMessage(board: Othello.Cell[][], cell: Othello.Cell) {
        return `${cell2Str(cell)}: ${Sum(board, r => Count(r, c => c == cell))}`
    }

    function Sum<T>(arr: T[], selector: (v: T) => number) {
        return arr.reduce((counter, item) => counter + selector(item), 0);
    }

    function Count<T>(arr: T[], filter: (v: T) => boolean) {
        return arr.reduce((counter, item) => filter(item) ? counter + 1 : counter, 0);
    }

    function OnTurnMessage(turn: Othello.Turn) {
        switch (turn.type) {
            case "OnTurn":
                return `${cell2Str(turn.chess)}'s turn`;
            case "End":
                if (turn.winner == "Draw")
                    return "Draw Game";
                else
                    return `${cell2Str(turn.winner)} Wins!`;
            case "Skip":
                return `${cell2Str(turn.chess)} must skip (s)he's turn`;
        }
    }

    const firstRow = `┌   ${[...Array(Othello.ColumnCount - 1)].map(_ => "┬").join("   ")}   ┐`;
    const midRow = `├   ${[...Array(Othello.ColumnCount - 1)].map(_ => "┼").join("   ")}   ┤`;
    const lastRow = `└   ${[...Array(Othello.ColumnCount - 1)].map(_ => "┴").join("   ")}   ┘`;
}

namespace Input {
    const Keys = {
        ctrl_c: '\u0003',
        esc: '\u001B',
        up: '\u001B\u005B\u0041',
        down: '\u001B\u005B\u0042',
        left: '\u001B\u005B\u0044',
        right: "\u001B\u005B\u0043",
        enter: ["\r", "\n", "\r\n"],
        retract: "b",
        redoRetracted: "r"
    };

    export const enum Directions { Up, Down, Left, Right }
    interface Click {
        type: "Click",
    }
    interface Direction {
        type: "Direction",
        direction: Directions,
    }
    interface Retract {
        type: "Retract"
    }interface RedoRetracted {
        type: "RedoRetracted"
    }
    interface Exit {
        type: "Exit"
    }
    export type Input = Click | Direction | Exit | Retract | RedoRetracted;
    export function Initialize(stream: NodeJS.Socket) {
        (<any>stream).setRawMode(true);
        stream.resume();
        stream.setEncoding('utf8');
    }
    export function Next(stream: NodeJS.Socket, onInput: (input: Input) => void) {
        const handler: (key: any) => void = key => {
            if (key === Keys.ctrl_c || key == Keys.esc)
                onInput({ type: "Exit" });
            else if (key == Keys.up)
                onInput({ type: "Direction", direction: Directions.Up });
            else if (key == Keys.down)
                onInput({ type: "Direction", direction: Directions.Down });
            else if (key == Keys.left)
                onInput({ type: "Direction", direction: Directions.Left });
            else if (key == Keys.right)
                onInput({ type: "Direction", direction: Directions.Right });
            else if (Keys.enter.find(k => k == key) != undefined)
                onInput({ type: "Click" })
            else if (key == Keys.retract)
                onInput({ type: "Retract" })
            else if (key == Keys.redoRetracted)
                onInput({ type: "RedoRetracted" })
            else
                stream.once('data', handler);
        }

        stream.once('data', handler);
    }
}

interface State {
    readonly game: Othello.Game
    readonly selected: Vector.Vector
    readonly retractStack: Stack<Othello.TurnAction>
    readonly messages?: string[]
}

function StateFrom(oldState: State, setters: Partial<State>) {
    return Object.assign({}, oldState, setters);
}

function ApplyInputOn(oldState: State, input: Input.Input): State {
    switch (input.type) {
        case "Exit":
            process.exit();
            throw Error("Exit");
        case "Direction":
            const vector = VectorForDirection(input.direction);
            const newSelected = Vector.Add(oldState.selected, vector);
            if (Othello.InBoard(newSelected))
                return StateFrom(oldState, { selected: newSelected });
            else
                return oldState;
        case "Click":
            const action: Othello.TurnAction = oldState.game.getCurrentTurn().type == "OnTurn" ?
                { type: "Movement", point: oldState.selected, chess: oldState.game.getCurrentTurn().chess }
                : { type: "ConfirmSkip", chess: oldState.game.getCurrentTurn().chess }
            const moveResult = oldState.game.applyAction(action);

            if (typeof (moveResult) == "string")
                return StateFrom(oldState, { messages: [moveResult] });
            else
                return StateFrom(oldState, { game: moveResult, messages: [] });
        case "Retract":
            const history = oldState.game.getHistory()
            if (history.length == 0)
                return StateFrom(oldState, { messages: ["Nothing to retract"] });
            const newRetractStack = oldState.retractStack.push(history[history.length - 1]);
            const newGame = history.slice(0, history.length - 1).reduce((g, a) => <Othello.Game>g.applyAction(a), OthelloImpl.NewGame);
            return StateFrom(oldState, { game: newGame, retractStack: newRetractStack, messages: [] });
        case "RedoRetracted":
            const action2Redo = oldState.retractStack.first();
            if (action2Redo == undefined)
                return StateFrom(oldState, { messages: ["Nothing to redo"] });
            return StateFrom(oldState, { game: <Othello.Game>oldState.game.applyAction(action2Redo), retractStack: oldState.retractStack.pop(), messages: [] });
    }
}

function VectorForDirection(dir: Input.Directions): Vector.Vector {
    switch (dir) {
        case Input.Directions.Up: return Vector.Create(-1, 0);
        case Input.Directions.Down: return Vector.Create(1, 0);
        case Input.Directions.Left: return Vector.Create(0, -1);
        case Input.Directions.Right: return Vector.Create(0, 1);

    }
}

function CreateInputHandler(state: State): (input: Input.Input) => void {
    return input => {
        const newState = ApplyInputOn(state, input);
        Graphic.Show(newState);
        if (newState.game.getCurrentTurn().type == "End")
            process.exit();
        else
            Input.Next(process.stdin, CreateInputHandler(newState));
    }
}

const intialState = { game: OthelloImpl.NewGame, retractStack: Stack<Othello.TurnAction>(), selected: Vector.Create(0, 0) }
Graphic.Show(intialState);
Input.Initialize(process.stdin);

Input.Next(process.stdin, CreateInputHandler(intialState));